package fractal.filter;

import java.awt.image.DataBuffer;
import java.util.Random;

import fractal.ColorMap;

public class CellularFilter extends FractalFilter implements Function2D
{
	protected float scale = 32;
	protected float stretch = 1.0f;
	protected float angle = 0.0f;
	public float amount = 1.0f;
	public float turbulence = 1.0f;
	public float gain = 0.5f;
	public float bias = 0.5f;
	public float distancePower = 2;
	public boolean useColor = false;
	protected Colormap colormap = new Gradient();
	protected float[] coefficients = {1, 0, 0, 0};
	protected float angleCoefficient;
	protected Random random = new Random();
	protected float m00 = 1.0f;
	protected float m01 = 0.0f;
	protected float m10 = 0.0f;
	protected float m11 = 1.0f;
	protected Point[] results = null;
	protected float randomness = 0;
	protected int gridType = HEXAGONAL;
//	private float min;
//	private float max;
	private static byte[] probabilities;
	private float gradientCoefficient;

	public final static int RANDOM = 0;
	public final static int SQUARE = 1;
	public final static int HEXAGONAL = 2;
	public final static int OCTAGONAL = 3;
	public final static int TRIANGULAR = 4;

	public CellularFilter()
	{
		results = new Point[3];
		for (int j = 0; j < results.length; j++)
			results[j] = new Point();
		if (probabilities == null)
		{
			probabilities = new byte[8192];
			float factorial = 1;
			float total = 0;
			float mean = 2.5f;
			for (int i = 0; i < 10; i++)
			{
				if (i > 1)
					factorial *= i;
				float probability = (float) Math.pow(mean, i)
						* (float) Math.exp(-mean) / factorial;
				int start = (int) (total * 8192);
				total += probability;
				int end = (int) (total * 8192);
				for (int j = start; j < end; j++)
					probabilities[j] = (byte) i;
			}
		}
	}

	/**
	 * Specifies the scale of the texture.
	 * 
	 * @param scale
	 *            the scale of the texture.
	 * @min-value 1
	 * @max-value 300+
	 * @see #getScale
	 */
	public void setScale(float scale)
	{
		this.scale = scale;
	}

	/**
	 * Returns the scale of the texture.
	 * 
	 * @return the scale of the texture.
	 * @see #setScale
	 */
	public float getScale()
	{
		return scale;
	}

	/**
	 * Specifies the stretch factor of the texture.
	 * 
	 * @param stretch
	 *            the stretch factor of the texture.
	 * @min-value 1
	 * @max-value 50+
	 * @see #getStretch
	 */
	public void setStretch(float stretch)
	{
		this.stretch = stretch;
	}

	/**
	 * Returns the stretch factor of the texture.
	 * 
	 * @return the stretch factor of the texture.
	 * @see #setStretch
	 */
	public float getStretch()
	{
		return stretch;
	}

	/**
	 * Specifies the angle of the texture.
	 * 
	 * @param angle
	 *            the angle of the texture.
	 * @angle
	 * @see #getAngle
	 */
	public void setAngle(float angle)
	{
		this.angle = angle;
		float cos = (float) Math.cos(angle);
		float sin = (float) Math.sin(angle);
		m00 = cos;
		m01 = sin;
		m10 = -sin;
		m11 = cos;
	}

	/**
	 * Returns the angle of the texture.
	 * 
	 * @return the angle of the texture.
	 * @see #setAngle
	 */
	public float getAngle()
	{
		return angle;
	}

	public void setCoefficient(int i, float v)
	{
		coefficients[i] = v;
	}

	public float getCoefficient(int i)
	{
		return coefficients[i];
	}

	public void setAngleCoefficient(float angleCoefficient)
	{
		this.angleCoefficient = angleCoefficient;
	}

	public float getAngleCoefficient()
	{
		return angleCoefficient;
	}

	public void setGradientCoefficient(float gradientCoefficient)
	{
		this.gradientCoefficient = gradientCoefficient;
	}

	public float getGradientCoefficient()
	{
		return gradientCoefficient;
	}

	public void setF1(float v)
	{
		coefficients[0] = v;
	}

	public float getF1()
	{
		return coefficients[0];
	}

	public void setF2(float v)
	{
		coefficients[1] = v;
	}

	public float getF2()
	{
		return coefficients[1];
	}

	public void setF3(float v)
	{
		coefficients[2] = v;
	}

	public float getF3()
	{
		return coefficients[2];
	}

	public void setF4(float v)
	{
		coefficients[3] = v;
	}

	public float getF4()
	{
		return coefficients[3];
	}

	/**
	 * Set the colormap to be used for the filter.
	 * 
	 * @param colormap
	 *            the colormap
	 * @see #getColormap
	 */
	public void setColormap(Colormap colormap)
	{
		this.colormap = colormap;
	}

	/**
	 * Get the colormap to be used for the filter.
	 * 
	 * @return the colormap
	 * @see #setColormap
	 */
	public Colormap getColormap()
	{
		return colormap;
	}

	public void setRandomness(float randomness)
	{
		this.randomness = randomness;
	}

	public float getRandomness()
	{
		return randomness;
	}

	public void setGridType(int gridType)
	{
		this.gridType = gridType;
	}

	public int getGridType()
	{
		return gridType;
	}

	public void setDistancePower(float distancePower)
	{
		this.distancePower = distancePower;
	}

	public float getDistancePower()
	{
		return distancePower;
	}

	/**
	 * Specifies the turbulence of the texture.
	 * 
	 * @param turbulence
	 *            the turbulence of the texture.
	 * @min-value 0
	 * @max-value 1
	 * @see #getTurbulence
	 */
	public void setTurbulence(float turbulence)
	{
		this.turbulence = turbulence;
	}

	/**
	 * Returns the turbulence of the effect.
	 * 
	 * @return the turbulence of the effect.
	 * @see #setTurbulence
	 */
	public float getTurbulence()
	{
		return turbulence;
	}

	/**
	 * Set the amount of effect.
	 * 
	 * @param amount
	 *            the amount
	 * @min-value 0
	 * @max-value 1
	 * @see #getAmount
	 */
	public void setAmount(float amount)
	{
		this.amount = amount;
	}

	/**
	 * Get the amount of texture.
	 * 
	 * @return the amount
	 * @see #setAmount
	 */
	public float getAmount()
	{
		return amount;
	}

	public class Point
	{
		public int index;
		public float x, y;
		public float dx, dy;
		public float cubeX, cubeY;
		public float distance;
	}

	private float checkCube(float x, float y, int cubeX, int cubeY,
			Point[] results)
	{
		int numPoints;
		random.setSeed(571 * cubeX + 23 * cubeY);
		switch (gridType)
		{
			case RANDOM :
			default :
				numPoints = probabilities[random.nextInt() & 0x1fff];
				break;
			case SQUARE :
				numPoints = 1;
				break;
			case HEXAGONAL :
				numPoints = 1;
				break;
			case OCTAGONAL :
				numPoints = 2;
				break;
			case TRIANGULAR :
				numPoints = 2;
				break;
		}
		for (int i = 0; i < numPoints; i++)
		{
			float px = 0, py = 0;
			float weight = 1.0f;
			switch (gridType)
			{
				case RANDOM :
					px = random.nextFloat();
					py = random.nextFloat();
					break;
				case SQUARE :
					px = py = 0.5f;
					if (randomness != 0)
					{
						px += randomness * (random.nextFloat() - 0.5);
						py += randomness * (random.nextFloat() - 0.5);
					}
					break;
				case HEXAGONAL :
					if ((cubeX & 1) == 0)
					{
						px = 0.75f;
						py = 0;
					} else
					{
						px = 0.75f;
						py = 0.5f;
					}
					if (randomness != 0)
					{
						px += randomness * Noise.noise2(271 * (cubeX + px),
								271 * (cubeY + py));
						py += randomness * Noise.noise2(271 * (cubeX + px) + 89,
								271 * (cubeY + py) + 137);
					}
					break;
				case OCTAGONAL :
					switch (i)
					{
						case 0 :
							px = 0.207f;
							py = 0.207f;
							break;
						case 1 :
							px = 0.707f;
							py = 0.707f;
							weight = 1.6f;
							break;
					}
					if (randomness != 0)
					{
						px += randomness * Noise.noise2(271 * (cubeX + px),
								271 * (cubeY + py));
						py += randomness * Noise.noise2(271 * (cubeX + px) + 89,
								271 * (cubeY + py) + 137);
					}
					break;
				case TRIANGULAR :
					if ((cubeY & 1) == 0)
					{
						if (i == 0)
						{
							px = 0.25f;
							py = 0.35f;
						} else
						{
							px = 0.75f;
							py = 0.65f;
						}
					} else
					{
						if (i == 0)
						{
							px = 0.75f;
							py = 0.35f;
						} else
						{
							px = 0.25f;
							py = 0.65f;
						}
					}
					if (randomness != 0)
					{
						px += randomness * Noise.noise2(271 * (cubeX + px),
								271 * (cubeY + py));
						py += randomness * Noise.noise2(271 * (cubeX + px) + 89,
								271 * (cubeY + py) + 137);
					}
					break;
			}
			float dx = Math.abs(x - px);
			float dy = Math.abs(y - py);
			float d;
			dx *= weight;
			dy *= weight;
			if (distancePower == 1.0f)
				d = dx + dy;
			else if (distancePower == 2.0f)
				d = (float) Math.sqrt(dx * dx + dy * dy);
			else
				d = (float) Math.pow(
						(float) Math.pow(dx, distancePower)
								+ (float) Math.pow(dy, distancePower),
						1 / distancePower);

			// Insertion sort the long way round to speed it up a bit
			if (d < results[0].distance)
			{
				Point p = results[2];
				results[2] = results[1];
				results[1] = results[0];
				results[0] = p;
				p.distance = d;
				p.dx = dx;
				p.dy = dy;
				p.x = cubeX + px;
				p.y = cubeY + py;
			} else if (d < results[1].distance)
			{
				Point p = results[2];
				results[2] = results[1];
				results[1] = p;
				p.distance = d;
				p.dx = dx;
				p.dy = dy;
				p.x = cubeX + px;
				p.y = cubeY + py;
			} else if (d < results[2].distance)
			{
				Point p = results[2];
				p.distance = d;
				p.dx = dx;
				p.dy = dy;
				p.x = cubeX + px;
				p.y = cubeY + py;
			}
		}
		return results[2].distance;
	}

	@Override
	public float evaluate(float x, float y)
	{
		for (int j = 0; j < results.length; j++)
			results[j].distance = Float.POSITIVE_INFINITY;

		int ix = (int) x;
		int iy = (int) y;
		float fx = x - ix;
		float fy = y - iy;

		float d = checkCube(fx, fy, ix, iy, results);
		if (d > fy)
			d = checkCube(fx, fy + 1, ix, iy - 1, results);
		if (d > 1 - fy)
			d = checkCube(fx, fy - 1, ix, iy + 1, results);
		if (d > fx)
		{
			checkCube(fx + 1, fy, ix - 1, iy, results);
			if (d > fy)
				d = checkCube(fx + 1, fy + 1, ix - 1, iy - 1, results);
			if (d > 1 - fy)
				d = checkCube(fx + 1, fy - 1, ix - 1, iy + 1, results);
		}
		if (d > 1 - fx)
		{
			d = checkCube(fx - 1, fy, ix + 1, iy, results);
			if (d > fy)
				d = checkCube(fx - 1, fy + 1, ix + 1, iy - 1, results);
			if (d > 1 - fy)
				d = checkCube(fx - 1, fy - 1, ix + 1, iy + 1, results);
		}

		float t = 0;
		for (int i = 0; i < 3; i++)
			t += coefficients[i] * results[i].distance;
		if (angleCoefficient != 0)
		{
			float angle = (float) Math.atan2(y - results[0].y,
					x - results[0].x);
			if (angle < 0)
				angle += 2 * (float) Math.PI;
			angle /= 4 * (float) Math.PI;
			t += angleCoefficient * angle;
		}
		if (gradientCoefficient != 0)
		{
			float a = 1 / (results[0].dy + results[0].dx);
			t += gradientCoefficient * a;
		}
		return t;
	}

	public float turbulence2(float x, float y, float freq)
	{
		float t = 0.0f;

		for (float f = 1.0f; f <= freq; f *= 2)
			t += evaluate(f * x, f * y) / f;
		return t;
	}

	public int getPixel(int x, int y, int[] inPixels, int width, int height)
	{
		float nx = m00 * x + m01 * y;
		float ny = m10 * x + m11 * y;
		nx /= scale;
		ny /= scale * stretch;
		nx += 1000;
		ny += 1000; // Reduce artifacts around 0,0
		float f = turbulence == 1.0f
				? evaluate(nx, ny)
				: turbulence2(nx, ny, turbulence);
		// Normalize to 0..1
//		f = (f-min)/(max-min);
		f *= 2;
		f *= amount;
		int a = 0xff000000;
		int v;
		if (colormap != null)
		{
			v = colormap.getColor(f);
			if (useColor)
			{
				int srcx = ImageMath.clamp(
						(int) ((results[0].x - 1000) * scale), 0, width - 1);
				int srcy = ImageMath.clamp(
						(int) ((results[0].y - 1000) * scale), 0, height - 1);
				v = inPixels[srcy * width + srcx];
				f = (results[1].distance - results[0].distance)
						/ (results[1].distance + results[0].distance);
				f = ImageMath.smoothStep(coefficients[1], coefficients[0], f);
				v = ImageMath.mixColors(f, 0xff000000, v);
			}
			return v;
		} else
		{
			v = clamp((int) (f * 255));
			int r = v << 16;
			int g = v << 8;
			int b = v;
			return a | r | g | b;
		}
	}

	@Override
	public CellularFilter clone()
	{
		CellularFilter f = (CellularFilter) super.clone();
		f.coefficients = coefficients.clone();
		f.results = results.clone();
		f.random = new Random();
		return f;
	}

//	Thread[] twoThreads2 = new Thread[2];
//	Thread[] fourThreads2 = new Thread[4];
//	Thread[] sixThreads2 = new Thread[6];
//	Runnable[] twoRuns2 = {new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(0, total / 2);
//		}
//	}, new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(total / 2, total);
//		}
//	}};
//
//	Runnable[] fourRuns2 = {new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(0, total / 4);
//		}
//	}, new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(total / 4, total / 2);
//		}
//	}, new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(total / 2, total - total / 4);
//		}
//	}, new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(total - total / 4, total);
//		}
//	}};
//
//	Runnable[] sixRuns2 = {new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(0, total / 6);
//		}
//	}, new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(total / 6, total / 3);
//		}
//	}, new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(total / 3, total / 2);
//		}
//	}, new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(total / 2, total - total / 3);
//		}
//	}, new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(total - total / 3, total - total / 6);
//		}
//	}, new Runnable()
//	{
//		@Override
//		public void run()
//		{
//			calculateOutput(total - total / 6, total);
//		}
//	}};

//	@Override
//	public synchronized void filter(int[] inPixels, int[] outPixels, int width,
//			int height, DataBuffer dataBuffer, int backColorPix,
//			ColorMap colorMap, int[] nDatas, int core)
//	{
//		if (inPixels == null)
//			return;
//		this.inPixels = inPixels;
//		total = width * height;
//		if (outPixels != null && outPixels.length >= total)
//			this.outPixels = outPixels;
//		else if (this.outPixels == null || this.outPixels.length != total)
//			this.outPixels = new int[total];
//		this.width = width;
//		this.height = height;
//		this.dataBuffer = dataBuffer;
//		this.backColorPix = backColorPix;
//		this.colorMap = colorMap;
//		this.nDatas = nDatas;
//		this.core = core;
//		initFilter();
//		if (core == 2)
//		{
//			for (int i = 0; i < twoRuns.length; i++)
//				twoThreads[i] = new Thread(twoRuns[i]);
//			ThreadTools.startAndJoin(twoThreads);
//		} else if (core == 4)
//		{
//			for (int i = 0; i < fourRuns.length; i++)
//				fourThreads[i] = new Thread(fourRuns[i]);
//			ThreadTools.startAndJoin(fourThreads);
//		} else if (core == 6)
//		{
//			for (int i = 0; i < sixRuns.length; i++)
//				sixThreads[i] = new Thread(sixRuns[i]);
//			ThreadTools.startAndJoin(sixThreads);
//		} else
//			calculateData(0, total);
//
//		if (core == 2)
//		{
//			for (int i = 0; i < twoRuns2.length; i++)
//				twoThreads2[i] = new Thread(twoRuns2[i]);
//			ThreadTools.startAndJoin(twoThreads2);
//		} else if (core == 4)
//		{
//			for (int i = 0; i < fourRuns2.length; i++)
//				fourThreads2[i] = new Thread(fourRuns2[i]);
//			ThreadTools.startAndJoin(fourThreads2);
//		} else if (core == 6)
//		{
//			for (int i = 0; i < sixRuns2.length; i++)
//				sixThreads2[i] = new Thread(sixRuns2[i]);
//			ThreadTools.startAndJoin(sixThreads2);
//		} else
//			calculateOutput(0, total);
//	}
//
//	@Override
//	protected void calculateData(int min, int max)
//	{
//		for (int i = min; i < max; i++)
//		{
//			outPixels[i] = calculatePixel(i);
//		}
//	}

	@Override
	public synchronized void filter(int[] inPixels, int[] outPixels, int width,
			int height, DataBuffer dataBuffer, int backColorPix,
			ColorMap colorMap, int[] nDatas, int core)
	{
		super.filter(inPixels, outPixels, width, height, dataBuffer,
				backColorPix, colorMap, nDatas, 1);
	}

	@Override
	protected void initFilter()
	{
		// TODO 自动生成的方法存根

	}

	@Override
	protected int calculatePixel(int i)
	{
		int x = i % width, y = i / width;
		float nx = m00 * x + m01 * y;
		float ny = m10 * x + m11 * y;
		nx /= scale;
		ny /= scale * stretch;
		nx += 1000;
		ny += 1000;
		float f = turbulence == 1.0f
				? evaluate(nx, ny)
				: turbulence2(nx, ny, turbulence);
		f *= 2;
		f *= amount;
		int a = 0xff000000;
		int v;
		if (colormap != null)
		{
			v = colormap.getColor(f);
			if (useColor)
			{
				int srcx = ImageMath.clamp(
						(int) ((results[0].x - 1000) * scale), 0, width - 1);
				int srcy = ImageMath.clamp(
						(int) ((results[0].y - 1000) * scale), 0, height - 1);
				v = inPixels[srcy * width + srcx];
				f = (results[1].distance - results[0].distance)
						/ (results[1].distance + results[0].distance);
				f = ImageMath.smoothStep(coefficients[1], coefficients[0], f);
				v = ImageMath.mixColors(f, 0xff000000, v);
			}
			return v;
		} else
		{
			v = clamp((int) (f * 255));
			int r = v << 16;
			int g = v << 8;
			int b = v;
			return a | r | g | b;
		}
	}

//	void calculateOutput(int min, int max)
//	{
//		if (dataBuffer != null)
//		{
//			if (colorMap != null)
//				scan : for (int i = min; i < max; i++)
//				{
//					if (outPixels[i] == 0)
//					{
//						dataBuffer.setElem(i, backColorPix);
//						continue scan;
//					} else
//					{
//						for (int j = 0; j < colorMap.length; j++)
//							if (nDatas[i] == colorMap.getKey(j))
//							{
//								dataBuffer.setElem(i, colorMap.getValue(j));
//								continue scan;
//							}
//					}
//					dataBuffer.setElem(i, outPixels[i]);
//				}
//			else
//				for (int i = min; i < max; i++)
//				{
//					if (inPixels[i] == 0)
//					{
//						dataBuffer.setElem(i, backColorPix);
//						continue;
//					}
//					dataBuffer.setElem(i, outPixels[i]);
//				}
//		} else
//		{
//			if (colorMap != null)
//				scan : for (int i = min; i < max; i++)
//				{
//					if (outPixels[i] == 0)
//					{
//						outPixels[i] = backColorPix;
//						continue scan;
//					} else
//					{
//						for (int j = 0; j < colorMap.length; j++)
//							if (nDatas[i] == colorMap.getKey(j))
//							{
//								outPixels[i] = colorMap.getValue(j);
//								continue scan;
//							}
//					}
//				}
//			else
//				for (int i = min; i < max; i++)
//					if (outPixels[i] == 0)
//						outPixels[i] = backColorPix;
//		}
//	}
}
